<?php
/**
 * @see Alinks_Post_Parser_Interface
 */
require_once('Alinks/Post/Parser/Interface.php');

/**
 * @see Alinks_Registry
 */
require_once('Alinks/Registry.php');

/**
 * Abstract class for the post parsing class(es).
 *
 * Copyright (C) 2008  Sean Hickey
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Sean Hickey <sean@headzoo.com>
 * @version $Id: Abstract.php 409 2008-07-05 14:51:48Z sean $
 * @package Alinks_Post
 */
abstract class Alinks_Post_Parser_Abstract implements Alinks_Post_Parser_Interface
{
	/**
	 * Don't play with these if you don't know what you're
	 * doing.
	 */
	//const REGEXP_PARSE = '#(?!((<h.*?)(<.*?)|(<a.*?)))(%s)(?!([^>]*?</h[1-6])|(([^<>]*?)>)|([^>]*?</a>))#i';
	const REGEXP_PARSE = '#(?!((<h.*?)(<.*?)|(<a.*?)))\b(%s)\b(?!([^>]*?</h[1-6])|(([^<>]*?)>)|([^>]*?</a>))#i';
	const REGEXP_PARSE_MATCH = 5;
	
	/**
	 * String found in posts that triggers aLinks to ignore the
	 * post.
	 */
	const IGNORE = '<!-- alinks ignore -->';
	
	/**
	 * Namespaces for caching
	 */
	const CACHE_NS_POSTS 	= 'posts';
	const CACHE_NS_COMMENTS	= 'comments';
	
	/**
	 * The Alinks_Cacher_Interface instace for caching content
	 *
	 * @var Alinks_Cacher_Interface
	 */
	protected $_cacher = null;
	
	/**
	 * The keyphrases to be parsed in the content
	 *
	 * @var Alinks_Keyphrase_Abstract[]
	 */
	protected $_keyphrases = null;
	
	/**
	 * The installed factories
	 *
	 * @var Alinks_Factory_Abstract[]
	 */
	protected $_factories = null;
	
	/**
	 * The link builder that will be used by factories
	 *
	 * @var Alinks_LinkBuilder
	 */
	protected $_link_builder;
	
	/**
	 * The aLinks settings
	 *
	 * @var array
	 */
	protected $_settings = null;
	
	/**
	 * Constructor
	 *
	 * @param Alinks_Keyphrase_Abstract[] $keyphrases The keyphrases to parse
	 * @param Alinks_Factory_Abstract[] $factories The installed factories
	 * @param Alinks_Cacher_Interface $cacher Cacher to use
	 */
	public function __construct(array $keyphrases = array(), array $factories = array(), Alinks_Cacher_Interface $cacher = null)
	{
		if (!empty($keyphrases)) $this->setKeyphrases($keyphrases);
		if (!empty($factories)) $this->setFactories($factories);
		if ($cacher) $this->setCacher($cacher);
	}
	
	/**
	 * Sets the keyphrases that are going be parsed in the content
	 *
	 * @param Alinks_Keyphrase_Abstract[] $keyphrases The keyphrases
	 * @return Alinks_Post_Parser_Abstract
	 */
	public function setKeyphrases(array $keyphrases)
	{
		foreach($keyphrases as $keyphrase) {
			$this->addKeyphrase($keyphrase);
		}
		return $this;
	}
	
	/**
	 * Sets the installed factories
	 *
	 * @param Alinks_Factory_Interface[] $factories The factories
	 * @return Alinks_Post_Parser_Abstract
	 */
	public function setFactories(array $factories)
	{
		foreach($factories as $factory) {
			$this->addFactory($factory);
		}
		return $this;
	}
	
	/**
	 * Sets the Alinks_Cacher_Interface that will be used to cache parsed
	 * content
	 *
	 * @param Alinks_Cacher_Interface $cacher
	 * @return Alinks_Post_Parser_Interface
	 */
	public function setCacher(Alinks_Cacher_Interface $cacher)
	{
		$this->_cacher = $cacher;
		return $this;
	}
	
	/**
	 * Sets an instance of Alinks_LinkBuilder_Interface for building links
	 *
	 * @param Alinks_LinkBuilder_Interface $link_builder The link builder
	 * @return Alinks_Post_Parser_Abstract
	 */
	public function setLinkBuilder(Alinks_LinkBuilder_Interface $link_builder)
	{
		$this->_link_builder = $link_builder;
		return $this;
	}
	
	/**
	 * Sets the aLinks settings
	 *
	 * @param array $settings The aLinks related user settings
	 * @return Alinks_Post_Parser_Abstract
	 */
	public function setSettings(array $settings)
	{
		$this->_settings = $settings;
		return $this;
	}
	
	/**
	 * Adds a keyphrase that to be parsed in the content
	 *
	 * @param Alinks_Keyphrase_Interface $keyphrase The keyphrase
	 * @return Alinks_Post_Parser_Abstract
	 */
	public function addKeyphrase(Alinks_Keyphrase_Interface $keyphrase)
	{
		if (!isset($this->_keyphrases[$keyphrase->getHash()])) {
			$this->_keyphrases[$keyphrase->getHash()] = $keyphrase;
		}
		return $this;
	}
	
	/**
	 * Adds a factory to parse the content
	 *
	 * @param Alinks_Factory_Interface $factory The factory
	 * @return Alinks_Post_Parser_Abstract
	 */
	public function addFactory(Alinks_Factory_Interface $factory)
	{
		if (!isset($this->_factories[$factory->getSafeName()])) {
			$this->_factories[$factory->getSafeName()] = $factory;
			$this->setKeyphrases($factory->getKeyphrases());
		}
		
		return $this;
	}
	
	/**
	 * Returns the keyphrases
	 *
	 * @return Alinks_Keyphrase_Abstract[]
	 */
	public function getKeyphrases()
	{
		if (null === $this->_keyphrases) {
			$keyphrases = Alinks_Registry::get('keyphrases', null);
			if (null === $keyphrases) {
				/**
				 * @see Alinks_Exception
				 */
				require_once('Alinks/Exception.php');
				throw new Alinks_Exception('Could not find keyphrases in the registry');
			}
			
			$this->setKeyphrases($keyphrases);
		}
		return $this->_keyphrases;
	}
	
	/**
	 * Returns the set factories
	 *
	 * @return Alinks_Factory_Abstract[]
	 */
	public function getFactories()
	{
		if (null === $this->_factories) {
			$this->_factories = Alinks_Registry::get('factories', null);
			if (null === $this->_factories) {
				/**
				 * @see Alinks_Exception
				 */
				require_once('Alinks/Exception.php');
				throw new Alinks_Exception('Could not find factories in the registry');
			}
		}
		return $this->_factories;
	}
	
	/**
	 * Returns the set Alinks_Cacher_Interface instance
	 *
	 * @return Alinks_Cacher_Interface
	 */
	public function getCacher()
	{
		return $this->_cacher;
	}
	
	/**
	 * Returns the link builder instance
	 *
	 * @return Alinks_LinkBuilder_Interface
	 */
	public function getLinkBuilder()
	{
		if (null === $this->_link_builder) {
			/**
			 * @see Alinks_LinkBuilder
			 */
			require_once('Alinks/LinkBuilder.php');
			$this->_link_builder = new Alinks_LinkBuilder();
		}
		
		return $this->_link_builder;
	}
	
	/**
	 * Returns the set settings
	 *
	 * @return array
	 */
	public function getSettings()
	{
		if (null === $this->_settings) {
			/**
			 * @see Alinks_Settings
			 */
			require_once('Alinks/Settings.php');
			$this->_settings = Alinks_Settings::get('alinks', null, array());
		}
		
		return $this->_settings;
	}
	
	/**
	 * Parses post content
	 *
	 * Replaces key phrases found in $content with links.
	 *
	 * @param string $content The content to parse
	 * @return string
	 */
	public function parse($content)
	{
		global $post, $comment;

		/**
		 * We will ignore this post if it contains the string <!-- alinks ignore -->
		 */
		if (stristr($content, self::IGNORE) !== false) {
			return str_replace(self::IGNORE, '', $content);
		}
		
		/**
		 * Check to see if this post or comment is cached. If so, return
		 * the cached content instead of parsing.
		 */ 
		if (null !== $this->_cacher && !Alinks_Registry::get('debugging', false)) {
			if (null !== $comment) {
				$ns = self::CACHE_NS_COMMENTS;
				$key = $comment->comment_ID;
			} else {
				$ns = self::CACHE_NS_POSTS;
				$key = $post->ID;
			}
			if ($this->_cacher->exists($key, $ns)) {
				return $this->_cacher->get($key, $ns);
			}
		}
		
		$this->getSettings();
		$this->getFactories();
		$this->getKeyphrases();
		$this->getLinkBuilder();

		/**
		 * Looks like there is no cached content, so we must parse!
		 */
		$matches = array();
		$total_link_count = 0;
		foreach($this->_keyphrases as $keyphrase) {
			/**
			 * Make sure the factory that created the keyphrase exists. We can't
			 * do anything without it. Also make sure the factory is ready to run
			 * by checking it's canRun() method.
			 */
			if (!isset($this->_factories[$keyphrase->getFactory()])) {
				continue;
			}
			$factory = $this->_factories[$keyphrase->getFactory()];
			if (!$factory->canRun()) {
				continue;
			}
			
			$phrase_found = true;
			$phrase_count = 0;
			$expression = sprintf(self::REGEXP_PARSE, $keyphrase->getPhraseRegexp());

			/**
			 * Keep replacing this phrase until it's not found in the post
			 * any longer, or the MAX_LINKS setting has been reached.
			 */
			while($phrase_found) {
				$phrase_found = preg_match($expression, $content, $matches);

				/**
				 * If the phrase was found, we set the text in the link
				 * builder to the text as it was found in the post, then
				 * pass the keyphrase and link builder to the factory.
				 */
				if ($phrase_found) {
					$this->_link_builder->reset();
					$this->_link_builder->addArg('title', $keyphrase->getDescription());
					if (isset($this->_settings[Alinks_Settings::NEW_WINDOW]) && $this->_settings[Alinks_Settings::NEW_WINDOW]) {
						$this->_link_builder->addArg('target', '_blank');
					}
					if (!empty($this->_settings[Alinks_Settings::INNER_HTML])) {
						$this->_link_builder->setInnerHtml($this->_settings[Alinks_Settings::INNER_HTML]);
					}
					if (!empty($this->_settings[Alinks_Settings::CLASSES])) {
						$this->_link_builder->setClasses(split("\t+", $this->_settings[Alinks_Settings::CLASSES]));
					}
					
					$this->_link_builder->setText($matches[self::REGEXP_PARSE_MATCH]);
					$factory->parse($keyphrase, $this->_link_builder);
					if ($this->_link_builder->getUri()) {
						$content = preg_replace(
							$expression,
							$this->_link_builder->getHtml(),
							$content,
							1
						);
						++$total_link_count;
						++$phrase_count;
					}

					if (isset($this->_settings[Alinks_Settings::MAX_LINKS_POST]) && $this->_settings[Alinks_Settings::MAX_LINKS_POST] != -1) {
						if ($total_link_count >= $this->_settings[Alinks_Settings::MAX_LINKS_POST]) {
							break 2;
						}
					}
					if (isset($this->_settings[Alinks_Settings::MAX_LINKS]) && $this->_settings[Alinks_Settings::MAX_LINKS] == -1) {
						continue;
					} else if ($phrase_count >= $this->_settings[Alinks_Settings::MAX_LINKS]) {
						break;
					}
				}
			}
		}
		
		/**
		 * Cache the content so we don't need to parse this post or
		 * comment again.
		 */
		$this->_cacher->set($key, $ns, $content, 3600);
		if (Alinks_Registry::get('debugging', false)) {
			$content = '<strong>Not From Cache</strong><br />' . $content;
		}
		
		return $content;
	}
	
	/**
	 * Called when a post is edited or deleted
	 *
	 * We need to clear the cache for that post if it's been
	 * changed.
	 *
	 * @param int $post_id The ID of the post
	 * @return int
	 */
	public function postUpdated($post_id)
	{
		if (null !== $this->_cacher) {
			if ($this->_cacher->exists($post_id, self::CACHE_NS_POSTS)) {
				$this->_cacher->delete($post_id, self::CACHE_NS_POSTS);
			}
		}
		
		return $post_id;
	}
	
	/**
	 * Called when a comment is edited or deleted
	 *
	 * We need to clear the cache for that comment if it's been
	 * changed.
	 *
	 * @param int $comment_id The ID of the comment
	 * @return int
	 */
	public function commentUpdated($comment_id)
	{
		if (null !== $this->_cacher) {
			if ($this->_cacher->exists($comment_id, self::CACHE_NS_COMMENTS)) {
				$this->_cacher->delete($comment_id, self::CACHE_NS_COMMENTS);
			}
		}
		
		return $comment_id;
	}
}